package lines.gameengine;

import lines.gameengine.context.FieldContextHolder;
import lines.gameengine.context.IFieldContext;
import lines.gameengine.exception.LinesEngineException;

import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;

import static lines.gameengine.context.FieldState.GAME_OVER;
import static lines.gameengine.context.FieldState.GAMING;
import static lines.gameengine.util.LinesEngineUtils.getEmptyCells;

/**
 * @author vpotapenko
 */
public class Field extends FieldContextHolder implements IField {

    private final CellsHolder cellsHolder = new CellsHolder();
    private final Collection<IFieldListener> listeners = new HashSet<IFieldListener>();

    private FieldPoint selectedPoint;

    public Field(IFieldContext context) {
        super(context);
    }

    @Override
    public void prepare() throws LinesEngineException {
        if (getContext().getFieldState() == GAMING)
            throw new LinesEngineException("Field already prepared");

        cellsHolder.initialize(getParameters().getWidth(), getParameters().getHeight());

        getContext().setFieldState(GAMING);
    }

    @Override
    public void reset() {
        cellsHolder.reset(getParameters().getWidth(), getParameters().getHeight());
        selectedPoint = null;

        getContext().setFieldState(GAMING);
    }

    /**
     * Returns cell by horizontal and vertical coordinates or <code>null</code>
     * if coordinates is invalid
     *
     * @param x horizontal coordinate
     * @param y vertical coordinate
     *
     * @return cell of field or <code>null</code>
     */
    @Override
    public ICell getCell(int x, int y) {
        return cellsHolder.getCell(x, y);
    }

    /**
     * Returns cell by field point or <code>null</code> if point is invalid
     *
     * @param point point of the cell
     * @return cell by field point or <code>null</code>
     */
    @Override
    public ICell getCell(FieldPoint point) {
        return cellsHolder.getCell(point);
    }

    /**
     * Returns number of horizontal cells in the field
     * @return number of horizontal cells in the field
     */
    @Override
    public int getWidth() {
        return cellsHolder.getWidth();
    }

    /**
     * Returns number of vertical cells in the field
     * @return number of vertical cells in the field
     */
    @Override
    public int getHeight() {
        return cellsHolder.getHeight();
    }

    @Override
    public IFieldContext getContext() {
        return super.getContext();
    }

    @Override
    public boolean isValidPoint(int x, int y) {
        return cellsHolder.isValidPoint(x, y);
    }

    @Override
    public boolean isValidPoint(FieldPoint point) {
        return point != null && isValidPoint(point.getX(), point.getY());
    }

    @Override
    public void clearCells(Collection<FieldPoint> points) {
        for (Iterator<FieldPoint> it = points.iterator(); it.hasNext();) {
            FieldPoint point = it.next();
            final Cell cell = cellsHolder.getCell(point);
            if (!cell.isEmpty()) {
                cell.clear();
            } else {
                it.remove();
            }
        }

        fireBallsClearedEvent(points);
    }

    @Override
    public void moveBall(FieldPoint startPoint, FieldPoint endPoint, List<FieldPoint> trace) {
        Cell startCell = cellsHolder.getCell(startPoint);
        Cell endCell = cellsHolder.getCell(endPoint);

        endCell.setBall(startCell.getBall());
        startCell.clear();

        fireBallMovedEvent(trace);
    }

    @Override
    public void addNewBall(FieldPoint point, IBall ball) throws LinesEngineException {
        if (getContext().getFieldState() == GAME_OVER)
            throw new LinesEngineException("Game is over");

        if (ball == null || !isValidPoint(point.getX(), point.getY())) return;

        Cell cell = cellsHolder.getCell(point);
        if (!cell.isEmpty())
            throw new LinesEngineException("Cell is not empty");

        cell.setBall(ball);

        fireNewBallEvent(point);
        checkGameOver();
    }

    @Override
    public void setSelectedPoint(FieldPoint point) {
        this.selectedPoint = point;

        firePointSelectedEvent();
    }

    private void firePointSelectedEvent() {
        for (IFieldListener l : listeners) {
            l.pointSelected(selectedPoint);
        }
    }

    @Override
    public FieldPoint getSelectedPoint() {
        return selectedPoint;
    }

    private void checkGameOver() {
        if (getEmptyCells(this).isEmpty()) getContext().setFieldState(GAME_OVER);
    }

    @Override
    public void addListener(IFieldListener l) {
        if (l != null) listeners.add(l);
    }

    @Override
    public void removeListener(IFieldListener l) {
        if (l != null) listeners.remove(l);
    }

    protected void fireNewBallEvent(FieldPoint point) {
        for (IFieldListener l : listeners) {
            l.newBallAdded(point);
        }
    }

    protected void fireBallsClearedEvent(Collection<FieldPoint> points) {
        for (IFieldListener l : listeners) {
            l.ballsCleared(points);
        }
    }

    protected void fireBallMovedEvent(List<FieldPoint> trace) {
        for (IFieldListener l : listeners) {
            l.ballMoved(trace);
        }
    }
}
